package com.hliushi.redis.controller;

import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * @author llhuang10
 * @date 2021/8/10 18:49
 */
@Slf4j
@RestController
@RequestMapping("/red")
@Api(description = "抢红包案例")
public class RedPacketController {

    public static final String RED_PACKET_CONSUME_KEY = "redpacket:consume:";
    public static final String RED_PACKET_KEY = "redpacket:";
    public static final String ID_KEY = "id:generator:redpacket";

    /**
     * 微信抢红包的技术实现原理
     * 包红包:
     * 1.先把金额拆解为小金额的红包, 例如总金额100元, 发20个, 用户在点保存的时候,
     * 就会自动拆解为20个随机小红包
     * 2.抢红包 高并发的抢红包时核心的关键技术, 就是控制各个小红包的原子性
     * 例如20个红包在500人的群里面被抢, 20个红包被抢走一个的同时要把红包的库存减1, 即剩下19个
     * 在整个过程中抢走一个和红包库存减1是一个原子性操作
     * #
     * 那那种数据类型符合"抢走一个和红包库存减1是一个原子操作" 采用 set? list? hash?
     * list比较适合  --> list的pop操作弹出一个元素的同时会自动从队列里面剔除该元素, 它是一个原子性操作
     */

    @Resource
    private RedisTemplate redisTemplate;


    /**
     * 包红包的接口
     *
     * @param total 金额总数
     * @param count 红包数量
     * @return
     */
    @PostMapping("/set")
    public ResponseEntity<Long> setRedpacket(Integer total, Integer count) {
        // 拆解红包
        Integer[] redPacket = this.splitRedPacket(total, count);
        // 为红包生成全局唯一Id
        long id = this.idGenerator();
        // 把红包放进 redis中list数据结构中
        String key = RED_PACKET_KEY + id;
        redisTemplate.opsForList().leftPushAll(key, redPacket);
        // 设置红包三天后过期
        redisTemplate.expire(key, 3, TimeUnit.DAYS);
        log.info("拆解红包{} = {}", key, redPacket);

        return ResponseEntity.ok(id);
    }

    /**
     * 抢红包接口
     *
     * @param userId 用户Id
     * @param redId  红包Id
     * @return
     */
    @GetMapping("/get")
    public Integer getRedPacket(Integer userId, Long redId) {
        // 1.判断该用户抢过红包没有
        Boolean hasKey = redisTemplate.opsForHash().hasKey(RED_PACKET_CONSUME_KEY + userId, String.valueOf(redId));
        // 2.当用户没有抢过红包
        if (!hasKey) {
            // 3.从redis中的list队列, 弹出一个红包
            Object redPacket = redisTemplate.opsForList().leftPop(RED_PACKET_KEY + redId);
            if (redPacket != null) {
                // 4.将强到红包保存起来
                redisTemplate.opsForHash().put(RED_PACKET_CONSUME_KEY + userId, String.valueOf(redId), redPacket);
                log.info("用户{} 抢到 {}", userId, redPacket);
                // TODO 异步把数据库落地到数据库上
                return (Integer) redPacket;
            } else {
                // -1 代表抢完
                log.info("{}红包已经抢完", redId);
                return -1;
            }
        }
        // -2 该用户代表以抢
        log.info("用户{}已抢过一次红包", userId);
        return -2;
    }

    /**
     * 生成全局唯一id
     *
     * @param
     * @return
     */
    public long idGenerator() {
        return redisTemplate.opsForValue().increment(ID_KEY);
    }

    /**
     * 拆解红包
     * 1.红包金额要被全部拆解完
     * 2.红包金额不能差太离谱
     */
    private Integer[] splitRedPacket(int total, int count) {
        int use = 0;
        Integer[] array = new Integer[count];
        Random random = new Random();
        for (int i = 0; i < count; i++) {
            if (i == count - 1)
                array[i] = total - use;
            else {
                // 2 红包随机金额浮动系数
                int avg = (total - use) * 2 / (count - i);
                array[i] = 1 + random.nextInt(avg - 1);
            }
            use = use + array[i];
        }
        return array;
    }
}
